1
Bergerak Menuju Grafik Berkinerja Tinggi
AI020Lesson 8
00:00

Dalam grafik komputer, kita membedakan antara Vektor dan Bitmap grafik. Grafik vektor (seperti SVG) menggambarkan gambar melalui bentuk logika; setiap elemen merupakan objek yang tetap di dalam DOM. Sebaliknya, grafik bitmap (seperti HTML5 Canvas) bekerja dengan raster titik berwarna.

1. Transisi ke Canvas

Meskipun SVG lebih mudah diberi gaya melalui CSS, browser harus melacak setiap simpul. Untuk kebutuhan kinerja tinggi, seperti game dengan ribuan bagian bergerak, API Canvas lebih unggul. Ini menyediakan satu elemen DOM yang menampung permukaan gambar—secara esensial sebuah "kertas kosong".

2. Konteks Menggambar

Elemen <canvas> adalah "kotak hitam" hingga kita menginisialisasi konteks. Metode objek ini menyediakan antarmuka penggambaran sebenarnya, memisahkan elemen tampilan dari logika rendering.

var konteks = canvas.getContext("2d");

3. Kesadaran Namespace

Dalam grafik berbasis XML seperti SVG, atribut xmlns="http://www.w3.org/2000/svg" merupakan hal yang krusial. Ini memberi sinyal kepada browser untuk beralih dari pemrosesan HTML standar ke skema grafik khusus, sehingga tag bentuk dapat dikenali sebagai objek interaktif.

main.py
TERMINALbash — 80x24
> Ready. Click "Run" to execute.
>
", "execution_steps": [ { "output": "Rendering SVG: Cyan circle (updated from red) and blue outlined square." }, { "output": "Rendering Canvas: Solid red rectangle drawn via pixel raster." } ] }; const executionSteps = (courseData && courseData.execution_steps) || []; // ── Machine Translation Data Hydration ── const domCode = document.getElementById('data-course-code'); if (domCode && courseData) { courseData.code = domCode.textContent; } const domExecs = document.querySelectorAll('#data-exec-output .data-exec'); domExecs.forEach(el => { const stepIdx = parseInt(el.getAttribute('data-step'), 10); if (!isNaN(stepIdx) && executionSteps[stepIdx]) { executionSteps[stepIdx].output = el.textContent.trim(); } }); const pageNumber = '1'; let visualMode = 'code'; let studyTimerInterval = null; let studyElapsedTime = 0; let studyLastStartTime = 0; let isStudyTimerRunning = false; let isSimRunning = false; let currentStep = 0; let timer = null; let animFrameId; let startTime = 0; // Global State let masterTimeline = null; let hybridInitialized = false; let totalTimelineSteps = 0; // tracks how many step labels the fallback timeline defines const els = { codeContainer: document.getElementById('code-container'), quizContainer: document.getElementById('quiz-container'), examContainer: document.getElementById('exam-container'), tabCode: document.getElementById('tab-code'), tabSim: document.getElementById('tab-sim'), tabQuiz: document.getElementById('tab-quiz'), tabExam: document.getElementById('tab-exam'), visControls: document.getElementById('vis-controls'), visStatusBar: document.getElementById('vis-status-bar'), code: document.getElementById('code-content'), visContainer: document.querySelector('.vis-container'), tipsBtn: document.getElementById('tips-btn'), drawer: document.getElementById('drawer-overlay'), timerDisplay: document.getElementById('study-timer'), timerVal: document.getElementById('timer-val'), consoleOutput: document.getElementById('console-output'), runBtn: document.getElementById('run-btn') }; // --- HELPERS --- /** * [通用渲染方法] General LaTeX Render Method * 封装 MathJax 渲染逻辑,可被多次调用 * @param {HTMLElement | Array} elements - 可选,指定渲染的元素或元素数组 */ function renderLaTeX(elements) { if (window.MathJax && typeof window.MathJax.typesetPromise === 'function') { // 如果传入了具体元素,只渲染这些元素 if (elements) { //确保 elements 是数组形式 const elArray = Array.isArray(elements) ? elements : [elements]; MathJax.typesetPromise(elArray).catch(err => console.warn('MathJax specific render error:', err)); } else { // 否则渲染全页 MathJax.typesetPromise().catch(err => console.warn('MathJax global render error:', err)); } } } // Syntax Highlighter adapted for Python keywords function renderCode(codeStr) { return codeStr.split('\n').map((line, i) => { const lineNum = i + 1; let htmlLine = ''; const regex = /((?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'))|(#.*)|(\b(?:def|class|if|else|elif|while|for|in|return|import|from|as|try|except|finally|with|lambda|pass|break|continue)\b)|(\b(?:print|len|range|enumerate|zip|map|filter|set|list|dict|int|str|float|sum|max|min|append|pop)\b)|(\b(?:True|False|None|[0-9]+)\b)|(\+|-|\*|\/|=|<|>|!|%|\[|\]|\{|\}|\(|\))/g; let lastIndex = 0; let match; while ((match = regex.exec(line)) !== null) { const textBefore = line.slice(lastIndex, match.index); htmlLine += escapeHtml(textBefore); const [fullMatch, str, com, kw, fn, num, op] = match; if (str) htmlLine += `${escapeHtml(str)}`; else if (com) htmlBaris += `${escapeHtml(com)}`; else if (kw) htmlBaris += `${escapeHtml(kw)}`; else if (fn) htmlBaris += `${escapeHtml(fn)}`; else if (num) htmlBaris += `${escapeHtml(num)}`; else if (op) htmlBaris += `${escapeHtml(op)}`; indeksTerakhir = regex.lastIndex; } htmlBaris += escapeHtml(baris.slice(indeksTerakhir)); return `
${nomorBaris}
${htmlBaris}
`; }).join(''); } function escapeHtml(teks) { if (!teks) return ''; return teks.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } // Bantuan untuk membersihkan semua sorotan // Bantuan untuk membersihkan semua sorotan function clearHighlights() { const baris = document.querySelectorAll('.code-line'); baris.forEach(l => l.classList.remove('executing')); } // --- LOGIKA INTERAKSI --- // BARU: Simulasi Lanjutan dengan Sorotan Baris window.runSimulatedCode = async function () { if (simulasiAktif) return; const terminal = els.outputKonsol; if (!terminal) return; simulasiAktif = true; if (els.tombolJalankan) els.tombolJalankan.disabled = true; try { // Keadaan Terminal Awal terminal.innerHTML = `
${courseData.run_cmd || '> python3 main.py'}
`; const tunggu = (ms) => new Promise(r => setTimeout(r, ms)); await tunggu(400); // Cetak semua hasil sekaligus (tanpa sorotan baris per baris) for (let i = 0; i < stepKesempatan.length; i++) { const langkah = stepKesempatan[i]; if (langkah.output) { const div = document.createElement('div'); div.className = 'console-line'; div.innerText = langkah.output; terminal.appendChild(div); } } // Selesai const kursor = document.createElement('div'); kursor.className = 'console-line'; kursor.innerHTML = `> `; terminal.appendChild(kursor); terminal.scrollTop = terminal.scrollHeight; } catch (err) { console.error("Kesalahan simulasi", err); } finally { simulasiAktif = false; if (els.tombolJalankan) els.tombolJalankan.disabled = false; } }; // Fungsi Salin Kode window.copyCode = function () { if (navigator.clipboard) { navigator.clipboard.writeText(courseData.kode).then(() => { const btn = document.querySelector('.ide-btn[onclick="copyCode()"]'); if (btn) { const parent = btn.parentElement; const htmlAsli = parent.innerHTML; parent.innerHTML = ` Disalin!`; lucide.createIcons(); setTimeout(() => { parent.innerHTML = htmlAsli; lucide.createIcons(); }, 2000); } }); } }; // Toggle Tantangan/Ujian // Logika Pemilihan Kuis function toggleFullScreen() { const elem = els.containerVisual; if (elem) { if (!document.fullscreenElement) { elem.requestFullscreen().catch(err => { console.error(err); }); } else { document.exitFullscreen(); } } } const fsBtn = document.getElementById('fsBtn'); if (fsBtn) { document.addEventListener('fullscreenchange', () => { const isFull = !!document.fullscreenElement; fsBtn.innerHTML = isFull ? '' : ''; if (window.lucide) lucide.createIcons(); }); } // Handler jawaban kuis (gaya referensi dari InnerPage General) window.checkAnswer = function(kartu, awalan, benar) { const grid = kartu.closest('.quiz-options-grid'); if (grid.classList.contains('answered')) return; grid.classList.add('answered'); kartu.classList.add('selected', benar ? 'correct' : 'incorrect'); const ikon = document.createElement('i'); ikon.setAttribute('data-lucide', benar ? 'check-circle' : 'x-circle'); ikon.style.flexShrink = '0'; kartu.appendChild(ikon); if (window.lucide) lucide.createIcons(); const kotakBenar = document.getElementById(awalan + '-correct'); const kotakSalah = document.getElementById(awalan + '-incorrect'); if (benar && kotakBenar) { kotakBenar.style.display = 'block'; } else if (!benar && kotakSalah) { kotakSalah.style.display = 'block'; } }; // Toggle solusi ujian (gaya referensi dari InnerPage General) window.toggleExamSolution = function(btn) { const jawabDiv = btn.nextElementSibling; const isVis = jawabDiv.classList.toggle('visible'); btn.innerHTML = isVis ? 'Sembunyikan Solusi ' : 'Show Solution '; if (window.lucide) lucide.createIcons(); if (isVis) renderLaTeX(jawabDiv); }; // --- LOGIKA APLIKASI --- // Atur ulang kartu kuis window.resetQuizCard = function(btn) { const kartu = btn.closest('.quiz-card'); if (!kartu) return; const grid = kartu.querySelector('.quiz-options-grid'); if (grid) { grid.classList.remove('answered'); grid.querySelectorAll('.quiz-opt-card').forEach(opt => { opt.classList.remove('selected', 'correct', 'incorrect'); const ikon = opt.querySelector('i'); if (ikon) ikon.remove(); }); } kartu.querySelectorAll('.quiz-feedback-box').forEach(fb => { fb.style.display = 'none'; fb.classList.remove('visible'); }); }; function init() { try { if (courseData.filename && document.getElementById('code-filename-display')) { document.getElementById('code-filename-display').textContent = courseData.filename; } loadContent(); } catch (e) { console.error("Gagal memuat konten", e); } try { initStudyTimer(); } catch (e) { console.error("Gagal menginisialisasi timer", e); } if (courseData.visual && courseData.visual.simStructure && !courseData.visual.simSteps) { courseData.visual.simSteps = []; } if (els.tabKode) els.tabKode.onclick = () => setVisualMode('kode'); if (els.tabSim) els.tabSim.onclick = () => setVisualMode('sim'); if (els.tabKuis) els.tabKuis.onclick = () => setVisualMode('kuis'); if (els.tabTantangan) els.tabTantangan.onclick = () => setVisualMode('tantangan'); if (document.getElementById('btn-play') && typeof togglePlay === 'function') document.getElementById('btn-play').onclick = togglePlay; if (document.getElementById('btn-next') && typeof step === 'function') document.getElementById('btn-next').onclick = () => step(1); if (document.getElementById('btn-prev') && typeof step === 'function') document.getElementById('btn-prev').onclick = () => step(-1); if (document.getElementById('btn-reset') && typeof resetSim === 'function') document.getElementById('btn-reset').onclick = resetSim; if (els.tombolTips) els.tombolTips.onclick = () => els.overlayDrawer.classList.add('active'); const tutupDrawer = document.getElementById('close-drawer'); if (tutupDrawer) tutupDrawer.onclick = () => els.overlayDrawer.classList.remove('active'); if (els.overlayDrawer) els.overlayDrawer.onclick = (e) => { if (e.target === els.overlayDrawer) els.overlayDrawer.classList.remove('active'); }; } function initStudyTimer() { startStudyTimer(); if (els.tampilanTimer) els.tampilanTimer.onclick = toggleStudyTimer; } function toggleStudyTimer() { if (timerStudiAktif) { pauseStudyTimer(); } else { startStudyTimer(); } } function startStudyTimer() { if (timerStudiAktif) return; timerStudiAktif = true; waktuMulaiTerakhir = Date.now(); if (els.tampilanTimer) els.tampilanTimer.classList.remove('paused'); updateTimerDisplay(); intervalTimerStudi = setInterval(updateTimerDisplay, 1000); } function pauseStudyTimer() { if (!timerStudiAktif) return; timerStudiAktif = false; waktuTersimpan += Date.now() - waktuMulaiTerakhir; clearInterval(intervalTimerStudi); if (els.tampilanTimer) els.tampilanTimer.classList.add('paused'); updateTimerDisplay(); } function updateTimerDisplay() { let totalMs = waktuTersimpan; if (timerStudiAktif) totalMs += (Date.now() - waktuMulaiTerakhir); const totalDetik = Math.floor(totalMs / 1000); const m = Math.floor(totalDetik / 60).toString().padStart(2, '0'); const s = (totalDetik % 60).toString().padStart(2, '0'); if (els.nilaiTimer) els.nilaiTimer.innerText = `${m}:${s}`; } function loadContent() { renderLaTeX(); if (courseData.kode && els.kode) els.kode.innerHTML = renderKode(courseData.kode); setVisualMode(modeVisual); } function setVisualMode(mode) { modeVisual = mode; if (els.tabKode) els.tabKode.classList.toggle('active', mode === 'kode'); if (els.tabSim) els.tabSim.classList.toggle('active', mode === 'sim'); if (els.tabKuis) els.tabKuis.classList.toggle('active', mode === 'kuis'); if (els.tabTantangan) els.tabTantangan.classList.toggle('active', mode === 'tantangan'); if (els.containerKode) els.containerKode.style.display = 'none'; if (els.containerKuis) els.containerKuis.style.display = 'none'; if (els.containerTantangan) els.containerTantangan.style.display = 'none'; if (els.canvas) els.canvas.style.display = 'none'; if (els.kontrolVisual) els.kontrolVisual.style.display = 'none'; if (els.statusBarVisual) els.statusBarVisual.style.display = 'none'; if (mode === 'kode') { if (els.containerKode) els.containerKode.style.display = 'flex'; } else if (mode === 'kuis') { if (els.containerKuis) { els.containerKuis.style.display = 'block'; renderLaTeX(els.containerKuis); } } else if (mode === 'tantangan') { if (els.containerTantangan) { els.containerTantangan.style.display = 'block'; renderLaTeX(els.containerTantangan); } } } init(); window.addEventListener('resize', () => {});